TTP Internals ============= This is to describe how TTP internals works, mainly to serve as a reference for the Author and other developers. Lazy loading system ------------------- TTP uses lazy loading to load helper functions for all its components. That is to speed up TTP library loading time and to make sure that only dependencies required for functions in use need to be installed on the system, e.g. if you do not use excel output formatter, no need to install ``openpyxl`` library. The way how lazy loader works is quite simple, work flow is: 0. Starting with version 0.7.0, TTP first checks if ``ttp_dict_cache.pickle`` file exists and loads ``_ttp_`` dictionary from it, if no such file found, TTP proceeds with below steps. 1. Scan all files in all folders of TTP module using ``ast`` built in library to extract all functions names and assignments. 2. Save reference to function names and file where that function found in a lazy load class 3. Use directory and function name as a keys and store lazy load class in ``_ttp_`` dictionary 4. On first call to the function, lazy load class will perform import on the file where function in question located and will update references in ``_ttp_`` dictionary to all functions imported from that file 5. Starting with version 0.7.0 added support for caching ``_ttp_`` dictionary using pickle module, as a result after steps 1-4 completed, ``_ttp_`` dictionary stored in ``ttp_dict_cache.pickle`` file under TTP module directory Implications of above process are: 1. To add new function to TTP, ones need to create .py file and place it in appropriate directory 2. The more files TTP need to scan the slower it will load, hence it make sense to combine functions of similar functionality in single file 3. All functions in single file will be imported on first call to any of the functions 4. Starting with version 0.7.0, usage of ``_ttp_`` dictionary cache improves TTP import performance by about 50ms 5. Because of caching, TTP no longer scans sub folders, this means that to make TTP to see and use new function, ``ttp_dict_cache.pickle`` file **must** be deleted, forcing TTP to rescan and cache new function. To handle cases where ``ttp_dict_cache.pickle`` cannot be saved under TTP module path, for instance - read only file system or lack of privileges, but caching ``_ttp_`` dictionary still desirable, starting with version 0.8.0 lazy import system checks ``TTPCACHEFOLDER`` environment variable for file system path location to load/save ``ttp_dict_cache.pickle`` file. Sometimes it is good to have name of TTP function to reference python reserved names, for instance ``set`` or ``del``, but, it is against best practices to name your functions with python well reserved names. At the same time, TTP does not call function directly but rather reference to function stored in ``_ttp_`` dictionary and that reference got called upon request. As a result ``_name_map_`` can be defined within .py file to map function names within that file to ``_ttp_`` dictionary keys. Consider this example :: _name_map_ = { "set_func": "set" } def set_func(): pass Here, ``set_func`` is function defined within file, on load TTP will add reference to that function under ``set`` key in a ``_ttp_`` dictionary using ``_name_map_`` _ttp_ (not so) dunder dictionary -------------------------------- The purpose of ``_ttp_`` is multi-fold: 1. TTP injects ``_ttp_`` dictionary into global name space of each file it imports, the same is true for macro functions. That way, functions or macro can reference one another through ``_ttp_`` dictionary without the need to explicitly define import statements. 2. Template global variables reference stored in ``_ttp_`` dictionary 3. Reference to various internal objects stored in ``_ttp_`` dictionary to work with them out of functions. ``_ttp_`` dictionary content is:: {'formatters': {'csv': , 'excel': , 'jinja2': , 'json': , 'n2g': , 'pprint': , 'raw': , 'table': , 'tabulate': , 'yaml': }, 'global_vars': {}, 'group': {'cerberus': , 'contains': , 'contains_val': , 'containsall': , 'del': , 'equal': , 'exclude': , 'exclude_val': , 'excludeall': , 'expand': , 'itemize': , 'lookup': , 'macro': , 'record': , 'set': , 'sformat': , 'to_int': , 'to_ip': , 'validate': , 'void': }, 'input': {'extract_commands': , 'macro': , 'test': }, 'lookup': {'geoip2_db_loader': }, 'macro': {}, 'match': {'append': , 'cidr_match': , 'contains': , 'contains_re': , 'count': , 'dns': , 'endswith_re': , 'equal': , 'exclude': , 'exclude_re': , 'geoip_lookup': , 'gpvlookup': , 'greaterthan': , 'ip_info': , 'is_ip': , 'isdigit': , 'item': , 'join': , 'joinmatches': , 'lessthan': , 'let': , 'lookup': , 'mac_eui': , 'macro': , 'notdigit': , 'notendswith_re': , 'notequal': , 'notstartswith_re': , 'prepend': , 'print': , 'rdns': , 'record': , 'replaceall': , 'resub': , 'resuball': , 'rlookup': , 'set': , 'sformat': , 'startswith_re': , 'to_cidr': , 'to_float': , 'to_int': , 'to_ip': , 'to_list': , 'to_net': , 'to_str': , 'to_unicode': , 'truncate': , 'unrange': , 'uptimeparse': , 'void': }, 'output': {'deepdiff': , 'dict_to_list': , 'is_equal': , 'macro': , 'traverse': , 'validate': }, 'patterns': {'get': }, 'python_major_version': 3, 'returners': {'file': , 'self': , 'syslog': , 'terminal': }, 'sources': {'hopper': , 'netmiko': , 'nornir': }, 'template_obj': {}, 'ttp_object': , 'utils': {'get_attributes': , 'guess': , 'load_csv': , 'load_files': , 'load_ini': , 'load_json': , 'load_python': , 'load_python_exec': , 'load_struct': , 'load_text': , 'load_yaml': }, 'variable': {'get_date': , 'get_time': , 'get_time_ns': , 'get_timestamp': , 'get_timestamp_iso': , 'get_timestamp_ms': , 'getfilename': , 'gethostname': }, 'vars': {}} All above functions contained within ``.py`` files and spread across respective directories of TTP module. Description of ``_ttp_`` dictionary keys: * ``global_vars`` - dictionary to store variables produced by ``record`` function, this dictionary accessible between templates * ``group`` - group function * ``formatters`` - formatter function * ``input`` - input functions * ``lookup`` - lookup functions, such as database loaders * ``macro`` - functions from template ```` tag * ``match`` - match variable functions * ``output`` - output functions * ``patterns`` - function to retrieve match variable regex patterns * ``python_major_version`` - integer 2 or 3, representing python major version * ``returners`` - output returner functions * ``sources`` - input source functions * ``template_obj`` - references to template object * ``ttp_object`` - reference to ttp parser object itself * ``utils`` - various utilities * ``variable`` - template variables getter function * ``vars`` - contains in run values of template variables - variables defined in ```` tag It is also possible to add custom function in ``_ttp_`` dictionary using ``add_function`` method. Cross referencing functions using _ttp_ dictionary -------------------------------------------------- As mentioned before, ``_ttp_`` dictionary injected in global name-space of loaded functions, allowing functions to have access one to another and TTP in-run objects. Sample example on how to use cross referencing within macro:: from ttp import ttp template = """ interface Lo0 ip address 124.171.238.50/29 ! interface Lo1 ip address 1.1.1.1/30 interface {{ interface }} ip address {{ ip }} def add_last_host(data): # function to add last host in subnet to results ip_obj, _ = _ttp_["match"]["to_ip"](data["ip"]) all_ips = list(ip_obj.network.hosts()) data["last_host"] = str(all_ips[-1]) return data """ parser = ttp(template=template) parser.parse() res = parser.result() pprint.pprint(res) Results:: [[[{'interface': 'Lo0', 'ip': '124.171.238.50/29', 'last_host': '124.171.238.54'}, {'interface': 'Lo1', 'ip': '1.1.1.1/30', 'last_host': '1.1.1.2'}]]] In this example ``_ttp_["match"]["to_ip"](data["ip"])`` is a call to match variable ``to_ip`` function to convert match result in IPv4Interface object. Methods of this object, for example, can be used to extract information about last host in the subnet.